using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;


namespace XmlObjects
{
  /// <summary>
  /// Class holding information on a specific field.
  /// Basically, it is an struct of three elements: a FieldInfo, the associated TypeHelper, and 
  ///   knowledge on whether the type is associated to an attribute or a XML element
  /// </summary>
  class FieldHandler
  {
    /// <summary>
    /// Only constructor.
    /// </summary>
    /// <param name="fi">The FieldInfo this FieldHandler represents</param>
    /// <param name="typeHelperHandler">The typeHelperHandler to use to study the type associated to this field</param>
    /// <param name="fieldType">The attribute associated to this field, that will determine
    ///   if the field is an attribute or an element.</param>
    /// <param name="attributeFields">The default value for the mapping of fields. If
    ///   is true, the enumerations, primitives and strings are mapped as attributes</param>
    public FieldHandler(FieldInfo fi, TypeHelperHandler factory,
      XmlFieldAttribute.XmlFieldType fieldType, bool attributeFields)
    {
      fieldInfo = fi;
      //if the type is an array, then it is never an attribute, but a nested xml element
      if (fi.FieldType.IsArray)
      {
        typeHelper = factory.CreateInstance(fi.FieldType.GetElementType());
      }
      else
      {
          if (fi.FieldType.IsGenericType) {
              //As special case, List<T> is supported directly
              Type[] genericArguments = fi.FieldType.GetGenericArguments(); //error if more than one!
              if (genericArguments.Length == 1 && fi.FieldType ==
                typeof(List<>).MakeGenericType(genericArguments)) {
                  Type constructorArguments = typeof(IEnumerable<>).MakeGenericType(genericArguments);
                  listConstructor = fi.FieldType.GetConstructor(new Type[] { constructorArguments });
                  if (listConstructor != null) typeHelper = factory.CreateInstance(genericArguments[0]);
              }
          }
        if (typeHelper == null)
        {
          typeHelper = factory.CreateInstance(fi.FieldType);
          switch (fieldType)
          {
            case XmlFieldAttribute.XmlFieldType.NotDefined:
              //The FieldHandler does not determine if a type is simple; instead, it 
              //delegates such decision to the TypeHelper.SimpleType property.
              //(SimpleType could decide that types with Parse method default to attributes)
              attribute = attributeFields && typeHelper.SimpleType;
              break;
            case XmlFieldAttribute.XmlFieldType.Attribute:
              attribute = true;
              break;
          }
        }
      }
    }



    /// <summary>
    /// Accessor to the TypeHelper embedded in the FieldHandler
    /// </summary>
    public TypeHelper TypeHelper
    {
      get { return typeHelper; }
    }



    /// <summary>
    /// Accessor to the FieldInfo embedded in the FieldHandler
    /// </summary>
    public FieldInfo FieldInfo
    {
      get { return fieldInfo; }
    }


    /// <summary>
    /// Returns the value of the field as a string
    /// </summary>
    /// <param name="instance"></param>
    /// <returns></returns>
    public string GetValue(object instance)
    {
      object value = fieldInfo.GetValue(instance);
      return value == null ? string.Empty : typeHelper.ConvertToString(value);
    }



    /// <summary>
    /// Method to determine if the associated field is mapped to an attribute or to a 
    ///   nested element
    /// </summary>
    /// <returns></returns>
    public bool IsAttribute()
    {
      return attribute;
    }


    /// <summary>
    /// Returns true if associated to an array type
    /// </summary>
    public bool IsArray()
    {
      return fieldInfo.FieldType.IsArray;
    }



    /// <summary>
    /// Sets the provided support field for this FieldHandler (if any)
    /// </summary>
    public FieldInfo ProvidedSupportField
    {
      set { providedSupportFieldInfo = value; }
    }


    /// <summary>
    /// Returns true if the field has been specified. It is not specified if
    ///   the associated provided support field is false or the value is null
    /// </summary>
    /// <param name="instance"></param>
    /// <returns></returns>
    public bool IsSpecified(object instance)
    {
      if (providedSupportFieldInfo == null)
        return fieldInfo.GetValue(instance) != null;
      return (bool)providedSupportFieldInfo.GetValue(instance);
    }


    /// <summary>
    /// Returns the values of the field as an Enumeration.
    /// If the field is null, or is an array with empty elements, the null elements
    ///   are not returned
    /// </summary>
    /// <param name="instance"></param>
    /// <returns></returns>
    public IEnumerable GetValues(object instance)
    {
      object value = fieldInfo.GetValue(instance);
      if (value == null)
        yield break;
      else if (fieldInfo.FieldType.IsArray || listConstructor!=null)
      {
        foreach (object v in (IEnumerable)value)
          if (v != null)
            yield return v;
      }
      else if (value != null)
        yield return value;
    }



    /// <summary>
    /// Method to set the value of the field handled by this FieldHandler, on the given targetObj
    /// The passed string is automatically parsed, if needed, to match the instance associated
    ///   to the FieldType
    /// </summary>
    /// <param name="targetObj">The object to update. It should match the type to which the 
    ///   FieldInfo belongs</param>
    /// <param name="text">The value to set, as a string.</param>
    public void SetValue(object targetObj, string text)
    {
      SetValue(targetObj, TypeHelper.CreateInstance(text));
    }


    /// <summary>
    /// Method to set the value of the field handled by this FieldHandler, on the given targetObj
    /// The value is passed as an ArrayList, where each element in the array has the same type
    ///   as the type associated to this FieldHandler.
    /// This method must be called when the field associated to the FieldHandler is an array; it
    ///   can be used as well if the field is not an array, but in that case the passed value
    ///   must have 1 only element, or be empty.
    /// </summary>
    /// <param name="targetObj">The object to update. It should match the type to which the 
    ///   FieldInfo belongs</param>
    /// <param name="value"></param>
    public void SetValue(object targetObj, ArrayList value)
    {
      object target;
      if (fieldInfo.FieldType.IsArray)
      {
        target = value.ToArray(typeHelper.XmlType);
      }
      else if (listConstructor != null)
      {
        target = listConstructor.Invoke(new object[] { value.ToArray(typeHelper.XmlType) });
      }
      else
      {
        switch (value.Count)
        {
          case 0:
            //not specified, do nothing
            return;
          case 1:
            target = value[0];
            break;
          default:
            throw new XmlObjectsException(XmlObjectsError.ArrayOnSimpleField, typeHelper.Name,
              fieldInfo.Name, value.Count);
        }
      }
      SetValue(targetObj, target);
    }


    /// <summary>
    /// Method to set the value of the field handled by this FieldHandler, on the given targetObj
    /// </summary>
    /// <param name="targetObj">The object to update. It should match the type to which the 
    ///   FieldInfo belongs</param>
    /// <param name="text"></param>
    public void SetValue(object targetObj, object value)
    {
      try
      {
        FieldInfo.SetValue(targetObj, value);
      }
      catch (ArgumentException)
      {
        //if the target objects does not match the expected type, a System.ArgumentException is
        // raised. This exception is handled as a InvalidFieldType case
        throw new XmlObjectsException(XmlObjectsError.InvalidFieldType, typeHelper.Name,
          fieldInfo.Name, value.GetType());
      }

      if (providedSupportFieldInfo != null)
        providedSupportFieldInfo.SetValue(targetObj, true);
    }



    private FieldInfo providedSupportFieldInfo;
    private FieldInfo fieldInfo;
    private TypeHelper typeHelper;
    private bool attribute;
    private ConstructorInfo listConstructor;
  }
}
